#!/bin/bash
# /usr/lib/check_mk_agent/local/300/check_crafty

set -u

CONFIG="/etc/check_mk/crafty.conf"

OK=0
WARN=1
CRIT=2
UNKNOWN=3

#
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
#

if [[ ! -r "$CONFIG" ]]; then
    printf '%s\n' "${UNKNOWN} \"Crafty\" - Configuration file ${CONFIG} missing or unreadable"
    exit 0
fi

# shellcheck disable=SC1090
source "$CONFIG"

CRAFTY_URL="${CRAFTY_URL:-https://127.0.0.1:8443}"
CRAFTY_USER="${CRAFTY_USER:-}"
CRAFTY_PASSWORD="${CRAFTY_PASSWORD:-}"
CRAFTY_INSECURE="${CRAFTY_INSECURE:-yes}"
IGNORE_HIDDEN_SERVERS="${IGNORE_HIDDEN_SERVERS:-yes}"

CHECK_UPDATES="${CHECK_UPDATES:-yes}"
CRAFTY_RELEASE_API="${CRAFTY_RELEASE_API:-https://gitlab.com/api/v4/projects/34675721/releases/permalink/latest}"

#
# ---------------------------------------------------------------------------
# Requirements
# ---------------------------------------------------------------------------
#

if ! command -v curl >/dev/null 2>&1; then
    printf '%s\n' "${UNKNOWN} \"Crafty\" - curl is not installed"
    exit 0
fi

if ! command -v jq >/dev/null 2>&1; then
    printf '%s\n' "${UNKNOWN} \"Crafty\" - jq is not installed"
    exit 0
fi

if [[ -z "$CRAFTY_USER" || -z "$CRAFTY_PASSWORD" ]]; then
    printf '%s\n' "${UNKNOWN} \"Crafty\" - CRAFTY_USER or CRAFTY_PASSWORD is not configured"
    exit 0
fi

#
# ---------------------------------------------------------------------------
# curl configuration
# ---------------------------------------------------------------------------
#

CURL_OPTS=(
    --silent
    --show-error
    --connect-timeout 3
    --max-time 10
)

if [[ "$CRAFTY_INSECURE" == "yes" ]]; then
    CURL_OPTS+=(--insecure)
fi

#
# ---------------------------------------------------------------------------
# Login
# ---------------------------------------------------------------------------
#

LOGIN_PAYLOAD="$(
    jq -n \
        --arg username "$CRAFTY_USER" \
        --arg password "$CRAFTY_PASSWORD" \
        '{
            username: $username,
            password: $password
        }'
)"

LOGIN_RESPONSE="$(
    curl \
        "${CURL_OPTS[@]}" \
        -X POST \
        -H "Content-Type: application/json" \
        -d "$LOGIN_PAYLOAD" \
        "${CRAFTY_URL%/}/api/v2/auth/login" \
        2>/dev/null
)"

CURL_RC=$?

if [[ $CURL_RC -ne 0 || -z "$LOGIN_RESPONSE" ]]; then
    printf '%s\n' \
        "${CRIT} \"Crafty\" - Unable to reach Crafty login API at ${CRAFTY_URL}"
    exit 0
fi

if ! printf '%s' "$LOGIN_RESPONSE" | jq -e . >/dev/null 2>&1; then
    printf '%s\n' \
        "${UNKNOWN} \"Crafty\" - Login API returned invalid JSON"
    exit 0
fi

LOGIN_STATUS="$(
    printf '%s' "$LOGIN_RESPONSE" |
        jq -r '.status // empty'
)"

TOKEN="$(
    printf '%s' "$LOGIN_RESPONSE" |
        jq -r '.data.token // empty'
)"

if [[ "$LOGIN_STATUS" != "ok" || -z "$TOKEN" ]]; then

    LOGIN_ERROR="$(
        printf '%s' "$LOGIN_RESPONSE" |
            jq -r '.error_data // .error // "authentication failed"'
    )"

    printf '%s\n' \
        "${CRIT} \"Crafty\" - Authentication failed: ${LOGIN_ERROR}"

    exit 0
fi

#
# ---------------------------------------------------------------------------
# Server inventory
# ---------------------------------------------------------------------------
#

SERVERS_RESPONSE="$(
    curl \
        "${CURL_OPTS[@]}" \
        -H "Authorization: Bearer ${TOKEN}" \
        "${CRAFTY_URL%/}/api/v2/servers" \
        2>/dev/null
)"

CURL_RC=$?

if [[ $CURL_RC -ne 0 ]]; then
    printf '%s\n' \
        "${CRIT} \"Crafty\" - Server inventory request failed, curl exit code ${CURL_RC}"
    exit 0
fi

if [[ -z "$SERVERS_RESPONSE" ]]; then
    printf '%s\n' \
        "${CRIT} \"Crafty\" - Server inventory API returned an empty response"
    exit 0
fi

if ! printf '%s' "$SERVERS_RESPONSE" | jq -e . >/dev/null 2>&1; then
    printf '%s\n' \
        "${UNKNOWN} \"Crafty\" - Server inventory returned invalid JSON"
    exit 0
fi

SERVERS_STATUS="$(
    printf '%s' "$SERVERS_RESPONSE" |
        jq -r '.status // empty'
)"

if [[ "$SERVERS_STATUS" != "ok" ]]; then

    SERVER_ERROR="$(
        printf '%s' "$SERVERS_RESPONSE" |
            jq -r '.error_data // .error // "unknown API error"'
    )"

    printf '%s\n' \
        "${CRIT} \"Crafty\" - Server inventory API error: ${SERVER_ERROR}"

    exit 0
fi

DATA_TYPE="$(
    printf '%s' "$SERVERS_RESPONSE" |
        jq -r '.data | type'
)"

if [[ "$DATA_TYPE" != "array" ]]; then
    printf '%s\n' \
        "${UNKNOWN} \"Crafty\" - Server inventory returned unexpected data type: ${DATA_TYPE}"
    exit 0
fi

#
# ---------------------------------------------------------------------------
# Server counts
# ---------------------------------------------------------------------------
#

TOTAL_CONFIGURED="$(
    printf '%s' "$SERVERS_RESPONSE" |
        jq '.data | length'
)"

if [[ "$IGNORE_HIDDEN_SERVERS" == "yes" ]]; then

    SERVER_COUNT="$(
        printf '%s' "$SERVERS_RESPONSE" |
            jq '[.data[] | select(.show_status != false)] | length'
    )"

else
    SERVER_COUNT="$TOTAL_CONFIGURED"
fi

#
# ---------------------------------------------------------------------------
# Crafty installed version via /metrics
# ---------------------------------------------------------------------------
#

METRICS_RESPONSE="$(
    curl \
        "${CURL_OPTS[@]}" \
        -H "Authorization: Bearer ${TOKEN}" \
        "${CRAFTY_URL%/}/metrics" \
        2>/dev/null
)"

METRICS_RC=$?

INSTALLED_VERSION=""

if [[ $METRICS_RC -eq 0 && -n "$METRICS_RESPONSE" ]]; then

    INSTALLED_VERSION="$(
        printf '%s\n' "$METRICS_RESPONSE" |
            grep -m1 '^Crafty_Controller_info{' |
            sed -n 's/.*version="\([^"]*\)".*/\1/p' |
            tr -d '[:space:]'
    )"

fi

#
# ---------------------------------------------------------------------------
# Overall Crafty service
# ---------------------------------------------------------------------------
#

if [[ -n "$INSTALLED_VERSION" ]]; then

    printf '%s\n' \
        "${OK} \"Crafty\" servers=${SERVER_COUNT};;;0 API reachable and authenticated - version ${INSTALLED_VERSION}, ${SERVER_COUNT}/${TOTAL_CONFIGURED} servers monitored"

else

    printf '%s\n' \
        "${OK} \"Crafty\" servers=${SERVER_COUNT};;;0 API reachable and authenticated - ${SERVER_COUNT}/${TOTAL_CONFIGURED} servers monitored"

fi

#
# ---------------------------------------------------------------------------
# Crafty update check
# ---------------------------------------------------------------------------
#

if [[ "$CHECK_UPDATES" == "yes" ]]; then

    if [[ -z "$INSTALLED_VERSION" ]]; then

        printf '%s\n' \
            "${UNKNOWN} \"Crafty Update\" - Unable to determine installed Crafty version from /metrics"

    else

        RELEASE_RESPONSE="$(
            curl \
                --silent \
                --show-error \
                --location \
                --fail \
                --connect-timeout 3 \
                --max-time 10 \
                -H "User-Agent: checkmk-crafty-local-check" \
                "$CRAFTY_RELEASE_API" \
                2>/dev/null
        )"

        RELEASE_RC=$?

        if [[ $RELEASE_RC -ne 0 || -z "$RELEASE_RESPONSE" ]]; then

            printf '%s\n' \
                "${UNKNOWN} \"Crafty Update\" - Installed ${INSTALLED_VERSION}, unable to query GitLab release API"

        elif ! printf '%s' "$RELEASE_RESPONSE" | jq -e . >/dev/null 2>&1; then

            printf '%s\n' \
                "${UNKNOWN} \"Crafty Update\" - GitLab release API returned invalid JSON"

        else

            LATEST_VERSION="$(
                printf '%s' "$RELEASE_RESPONSE" |
                    jq -r '.tag_name // empty'
            )"

            LATEST_VERSION="${LATEST_VERSION#v}"

            if [[ -z "$LATEST_VERSION" ]]; then

                printf '%s\n' \
                    "${UNKNOWN} \"Crafty Update\" - Unable to determine latest Crafty release"

            elif [[ ! "$INSTALLED_VERSION" =~ ^[0-9]+([.][0-9]+)*$ ]]; then

                printf '%s\n' \
                    "${UNKNOWN} \"Crafty Update\" - Unable to parse installed version ${INSTALLED_VERSION}"

            elif [[ ! "$LATEST_VERSION" =~ ^[0-9]+([.][0-9]+)*$ ]]; then

                printf '%s\n' \
                    "${UNKNOWN} \"Crafty Update\" - Unable to parse latest version ${LATEST_VERSION}"

            else

                NEWEST="$(
                    printf '%s\n%s\n' \
                        "$INSTALLED_VERSION" \
                        "$LATEST_VERSION" |
                        sort -V |
                        tail -n 1
                )"

                if [[ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then

                    printf '%s\n' \
                        "${OK} \"Crafty Update\" - Current: installed ${INSTALLED_VERSION}, latest ${LATEST_VERSION}"

                elif [[ "$NEWEST" == "$LATEST_VERSION" ]]; then

                    printf '%s\n' \
                        "${WARN} \"Crafty Update\" - Update available: installed ${INSTALLED_VERSION}, latest ${LATEST_VERSION}"

                else

                    printf '%s\n' \
                        "${OK} \"Crafty Update\" - Installed ${INSTALLED_VERSION} is newer than latest stable ${LATEST_VERSION}"

                fi
            fi
        fi
    fi
fi

#
# ---------------------------------------------------------------------------
# Individual Minecraft servers
# ---------------------------------------------------------------------------
#

while IFS= read -r SERVER; do

    SERVER_ID="$(
        printf '%s' "$SERVER" |
            jq -r '.server_id // empty'
    )"

    SERVER_NAME="$(
        printf '%s' "$SERVER" |
            jq -r '.server_name // "Unknown"'
    )"

    SERVER_PORT="$(
        printf '%s' "$SERVER" |
            jq -r '.server_port // 0'
    )"

    AUTO_START="$(
        printf '%s' "$SERVER" |
            jq -r '.auto_start // false'
    )"

    CRASH_DETECTION="$(
        printf '%s' "$SERVER" |
            jq -r '.crash_detection // false'
    )"

    COUNT_PLAYERS="$(
        printf '%s' "$SERVER" |
            jq -r '.count_players // false'
    )"

    SHOW_STATUS="$(
        printf '%s' "$SERVER" |
            jq -r '.show_status // true'
    )"

    if [[ "$IGNORE_HIDDEN_SERVERS" == "yes" && "$SHOW_STATUS" == "false" ]]; then
        continue
    fi

    SERVER_NAME="${SERVER_NAME//\"/\'}"

    if [[ -z "$SERVER_ID" ]]; then

        printf '%s\n' \
            "${UNKNOWN} \"Minecraft ${SERVER_NAME}\" - Server entry does not contain a server ID"

        continue
    fi

    #
    # -----------------------------------------------------------------------
    # Runtime statistics
    # -----------------------------------------------------------------------
    #

    STATS_RESPONSE="$(
        curl \
            "${CURL_OPTS[@]}" \
            -H "Authorization: Bearer ${TOKEN}" \
            "${CRAFTY_URL%/}/api/v2/servers/${SERVER_ID}/stats" \
            2>/dev/null
    )"

    STATS_RC=$?

    if [[ $STATS_RC -ne 0 || -z "$STATS_RESPONSE" ]]; then

        printf '%s\n' \
            "${UNKNOWN} \"Minecraft ${SERVER_NAME}\" - Unable to retrieve Crafty server statistics"

        continue
    fi

    if ! printf '%s' "$STATS_RESPONSE" | jq -e . >/dev/null 2>&1; then

        printf '%s\n' \
            "${UNKNOWN} \"Minecraft ${SERVER_NAME}\" - Statistics API returned invalid JSON"

        continue
    fi

    STATS_STATUS="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '.status // empty'
    )"

    if [[ "$STATS_STATUS" != "ok" ]]; then

        STATS_ERROR="$(
            printf '%s' "$STATS_RESPONSE" |
                jq -r '.error_data // .error // "unknown API error"'
        )"

        printf '%s\n' \
            "${UNKNOWN} \"Minecraft ${SERVER_NAME}\" - Crafty statistics API error: ${STATS_ERROR}"

        continue
    fi

    #
    # -----------------------------------------------------------------------
    # Runtime values
    # -----------------------------------------------------------------------
    #

    RUNNING="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '.data.running // false'
    )"

    CRASHED="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '.data.crashed // false'
    )"

    UPDATING="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '.data.updating // false'
    )"

    WAITING_START="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '.data.waiting_start // false'
    )"

    IMPORTING="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '.data.importing // false'
    )"

    FIRST_RUN="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '.data.first_run // false'
    )"

    CPU="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '.data.cpu // 0'
    )"

    MEMORY="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '.data.mem // 0'
    )"

    MEMORY_PERCENT="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '.data.mem_percent // 0'
    )"

    PLAYERS_ONLINE="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '
                if (.data.online | type) == "number"
                then .data.online
                else 0
                end
            '
    )"

    PLAYERS_MAX="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '
                if (.data.max | type) == "number"
                then .data.max
                else 0
                end
            '
    )"

    WORLD_NAME="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '
                if (.data.world_name // "") == ""
                then "unknown"
                else .data.world_name
                end
            '
    )"

    WORLD_SIZE="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '
                if (.data.world_size // "") == ""
                then "unknown"
                else .data.world_size
                end
            '
    )"

    MC_VERSION="$(
        printf '%s' "$STATS_RESPONSE" |
            jq -r '
                if (
                    .data.version == false or
                    .data.version == null or
                    .data.version == ""
                )
                then ""
                else (.data.version | tostring)
                end
            '
    )"

    #
    # -----------------------------------------------------------------------
    # Sanitize performance values
    # -----------------------------------------------------------------------
    #

    if ! [[ "$CPU" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
        CPU=0
    fi

    if ! [[ "$MEMORY" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
        MEMORY=0
    fi

    if ! [[ "$MEMORY_PERCENT" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
        MEMORY_PERCENT=0
    fi

    if ! [[ "$PLAYERS_ONLINE" =~ ^[0-9]+$ ]]; then
        PLAYERS_ONLINE=0
    fi

    if ! [[ "$PLAYERS_MAX" =~ ^[0-9]+$ ]]; then
        PLAYERS_MAX=0
    fi

    #
    # -----------------------------------------------------------------------
    # Determine CheckMK state
    # -----------------------------------------------------------------------
    #

    STATE=$OK
    STATUS_TEXT="Stopped"

    if [[ "$CRASHED" == "true" ]]; then

        STATE=$CRIT
        STATUS_TEXT="Crashed"

    elif [[ "$UPDATING" == "true" ]]; then

        STATUS_TEXT="Updating"

    elif [[ "$IMPORTING" == "true" ]]; then

        STATUS_TEXT="Importing"

    elif [[ "$WAITING_START" == "true" ]]; then

        STATUS_TEXT="Waiting to start"

    elif [[ "$RUNNING" == "true" ]]; then

        STATUS_TEXT="Running"

    fi

    #
    # -----------------------------------------------------------------------
    # Performance data
    # -----------------------------------------------------------------------
    #

    PERFDATA="cpu=${CPU}%;;;0;100|memory=${MEMORY};;;0|memory_percent=${MEMORY_PERCENT}%;;;0;100"

    if [[ "$COUNT_PLAYERS" == "true" ]]; then

        if [[ "$PLAYERS_MAX" -gt 0 ]]; then
            PERFDATA="${PERFDATA}|players=${PLAYERS_ONLINE};;;0;${PLAYERS_MAX}"
        else
            PERFDATA="${PERFDATA}|players=${PLAYERS_ONLINE};;;0"
        fi

    fi

    #
    # -----------------------------------------------------------------------
    # Human-readable output
    # -----------------------------------------------------------------------
    #

    DETAILS="$STATUS_TEXT"

    if [[ "$RUNNING" == "true" ]]; then

        if [[ "$COUNT_PLAYERS" == "true" ]]; then

            if [[ "$PLAYERS_MAX" -gt 0 ]]; then
                DETAILS="${DETAILS} - players ${PLAYERS_ONLINE}/${PLAYERS_MAX}"
            else
                DETAILS="${DETAILS} - players ${PLAYERS_ONLINE}"
            fi

        fi

        if [[ -n "$MC_VERSION" ]]; then
            DETAILS="${DETAILS}, version ${MC_VERSION}"
        fi

        DETAILS="${DETAILS}, CPU ${CPU}%, memory ${MEMORY_PERCENT}%"
    fi

    if [[ "$WORLD_NAME" != "unknown" ]]; then
        DETAILS="${DETAILS}, world ${WORLD_NAME}"
    fi

    if [[ "$WORLD_SIZE" != "unknown" ]]; then
        DETAILS="${DETAILS} (${WORLD_SIZE})"
    fi

    DETAILS="${DETAILS}, port ${SERVER_PORT}"

    if [[ "$AUTO_START" == "true" ]]; then
        DETAILS="${DETAILS}, auto-start enabled"
    fi

    if [[ "$CRASH_DETECTION" == "true" ]]; then
        DETAILS="${DETAILS}, crash detection enabled"
    fi

    if [[ "$FIRST_RUN" == "true" ]]; then
        DETAILS="${DETAILS}, first-run flag set"
    fi

    printf '%s\n' \
        "${STATE} \"Minecraft ${SERVER_NAME}\" ${PERFDATA} ${DETAILS}"

done < <(
    printf '%s' "$SERVERS_RESPONSE" |
        jq -c '.data[]'
)

exit 0